package gr.uoa.di.thanos.botcraft.game;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.PointerBuffer;
import org.lwjgl.opencl.CLCommandQueue;
import org.lwjgl.opencl.CLContext;
import org.lwjgl.opencl.CLKernel;
import org.lwjgl.opencl.CLMem;
import org.lwjgl.opencl.CLProgram;
import org.lwjgl.opencl.Util;
import org.lwjgl.util.Renderable;

import gr.uoa.di.thanos.botcraft.OpenCLUtils;
import gr.uoa.di.thanos.botcraft.OpenGLUtils;
import static org.lwjgl.opencl.CL10.*;
import static org.lwjgl.opencl.CL10GL.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.*;

/**
 * This class represents a map.
 * @author thanos.papapetrou@gmail.com
 *
 */
public class Map implements Renderable {
	private enum Direction {
		NORTH,
		NORTHEAST,
		EAST,
		SOUTHEAST,
		SOUTH,
		SOUTHWEST,
		WEST,
		NORTHWEST,
		CENTER
	}
		
	private final int latitudinalSize;
	private final int longitudinalSize;
	private final FloatBuffer altitudes;
	private final Terrain[] terrains;
	private int indexBuffer;
	private int vertexBuffer;
	private int normalBuffer;
	
	/**
	 * Construct a new map.
	 * @param latitudinalSize the map size across the north - south direction in meters
	 * @param longitudinalSize the map size across the east - west direction in meters
	 * @param altitudes the map altitudes in meters
	 * @param terrains the map terrains
	 */
	public Map(final int latitudinalSize, final int longitudinalSize, final FloatBuffer altitudes, final Terrain[] terrains) {
		if (latitudinalSize <= 0)
			throw new IllegalArgumentException(Map.class.getName() + " latitudinal size must be positive");
		if (longitudinalSize <= 0)
			throw new IllegalArgumentException(Map.class.getName() + " longitudinal size must be positive");
		if (altitudes.capacity() != latitudinalSize * longitudinalSize)
			throw new IllegalArgumentException(Map.class.getName() + " altitudes must have exactly latitudinal size * longitudinal size elements");
		if (terrains.length != latitudinalSize * longitudinalSize)
			throw new IllegalArgumentException(Map.class.getName() + " terrains must have exactly latitudinal size * longitudinal size elements");
		this.latitudinalSize = latitudinalSize;
		this.longitudinalSize = longitudinalSize;
		this.altitudes = altitudes;
		this.terrains = terrains;
		indexBuffer = 0;
		vertexBuffer = 0;
		normalBuffer = 0;
	}
	
	private FloatBuffer vertices; // TODO
	private FloatBuffer normals;
	
	public void initialize(final CLContext context, final boolean shareGlClContext) {
		Terrain.initialize();
		final CLProgram program = OpenCLUtils.compileCL("/cl/map.cl", context, context.getInfoDevices().get(0));
		final CLKernel kernel = clCreateKernel(program, "map", null);
		final CLMem altitudeMemory = clCreateBuffer(context, CL_MEM_WRITE_ONLY | CL_MEM_USE_HOST_PTR, altitudes, null);
		kernel.setArg(0, altitudeMemory);
		final IntBuffer indices = BufferUtils.createIntBuffer(latitudinalSize * longitudinalSize * Direction.CENTER.ordinal() * OpenGLUtils.TRIANGLE_VERTICES);
		/*final FloatBuffer*/ vertices = BufferUtils.createFloatBuffer(latitudinalSize * longitudinalSize * Direction.values().length * OpenGLUtils.VERTEX_COORDINATES); // TODO
		/*final FloatBuffer*/ normals = BufferUtils.createFloatBuffer(latitudinalSize * longitudinalSize * Direction.values().length * OpenGLUtils.VERTEX_COORDINATES);
		indexBuffer = glGenBuffers();
		vertexBuffer = glGenBuffers();
		normalBuffer = glGenBuffers();
		CLMem indexMemory = null;
		CLMem vertexMemory = null;
		CLMem normalMemory = null;
		if (shareGlClContext && (context.getInfoDevices().get(0).getInfoInt(CL_DEVICE_TYPE) == CL_DEVICE_TYPE_GPU)) { // if device is GPU memory may be shared between OpenGL and OpenCL
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
			glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW);
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
			indexMemory = clCreateFromGLBuffer(context, CL_MEM_WRITE_ONLY, indexBuffer, null);
			glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
			vertices.rewind();
			glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
			glBindBuffer(GL_ARRAY_BUFFER, 0);
			vertexMemory = clCreateFromGLBuffer(context, CL_MEM_READ_WRITE, vertexBuffer, null);
			glBindBuffer(GL_ARRAY_BUFFER, normalBuffer);
			normals.rewind();
			glBufferData(GL_ARRAY_BUFFER, normals, GL_STATIC_DRAW);
			glBindBuffer(GL_ARRAY_BUFFER, 0);
			normalMemory = clCreateFromGLBuffer(context, CL_MEM_WRITE_ONLY, normalBuffer, null);
		} else { // use discrete memory areas for OpenCL and OpenGL
			indexMemory = clCreateBuffer(context, CL_MEM_WRITE_ONLY | CL_MEM_USE_HOST_PTR, indices, null);
			vertexMemory = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR, vertices, null);
			normalMemory = clCreateBuffer(context, CL_MEM_WRITE_ONLY | CL_MEM_USE_HOST_PTR, normals, null);
		}
		kernel.setArg(1, indexMemory);
		kernel.setArg(2, vertexMemory);
		kernel.setArg(3, normalMemory);
		final PointerBuffer globalWorkSize = BufferUtils.createPointerBuffer(3);
		globalWorkSize.put(0, latitudinalSize);
		globalWorkSize.put(1, longitudinalSize);
		globalWorkSize.put(2, Direction.values().length);
		final CLCommandQueue queue = clCreateCommandQueue(context, context.getInfoDevices().get(0), 0, null);
		if (shareGlClContext && (context.getInfoDevices().get(0).getInfoInt(CL_DEVICE_TYPE) == CL_DEVICE_TYPE_GPU)) { // if sharing memory between OpenGL and OpenCL
			clEnqueueAcquireGLObjects(queue, indexMemory, null, null);
			clEnqueueAcquireGLObjects(queue, vertexMemory, null, null);
			clEnqueueAcquireGLObjects(queue, normalMemory, null, null);
		}
		clEnqueueNDRangeKernel(queue, kernel, globalWorkSize.capacity(), null, globalWorkSize, null, null, null);
		if (shareGlClContext && (context.getInfoDevices().get(0).getInfoInt(CL_DEVICE_TYPE) == CL_DEVICE_TYPE_GPU)) { // if sharing memory between OpenGL and OpenCL
			clEnqueueReleaseGLObjects(queue, normalMemory, null, null);
			clEnqueueReleaseGLObjects(queue, vertexMemory, null, null);
			clEnqueueReleaseGLObjects(queue, indexMemory, null, null);
		}
		clFinish(queue);
		clReleaseCommandQueue(queue);
		clReleaseMemObject(normalMemory);
		clReleaseMemObject(vertexMemory);
		clReleaseMemObject(indexMemory);
		clReleaseMemObject(altitudeMemory);
		clReleaseKernel(kernel);
		clReleaseProgram(program);
		if ((!shareGlClContext) || (context.getInfoDevices().get(0).getInfoInt(CL_DEVICE_TYPE) != CL_DEVICE_TYPE_GPU)) { // not sharing memory between OpenGL and OpenCL copy OpenCL memory to OpenGL memory
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
			indices.rewind();
			glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW);
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
			glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
			vertices.rewind();
			glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
			glBindBuffer(GL_ARRAY_BUFFER, 0);
			glBindBuffer(GL_ARRAY_BUFFER, normalBuffer);
			normals.rewind();
			glBufferData(GL_ARRAY_BUFFER, normals, GL_STATIC_DRAW);
			glBindBuffer(GL_ARRAY_BUFFER, 0);
		}
// TODO debug
//System.out.println("Indices:");
//for (int i = 0; i < indices.capacity(); i += 3)
//System.out.println(indices.get(i) + " " + indices.get(i + 1) + " " + indices.get(i + 2));
//System.out.println("\nVertices:");
//for (int i = 0; i < vertices.capacity(); i += 3)
//System.out.println(vertices.get(i) + " " + vertices.get(i + 1) + " " + vertices.get(i + 2));
//System.out.println("\nNormals:");
//for (int i = 0; i < normals.capacity(); i += 3)
//System.out.println(normals.get(i) + " " + normals.get(i + 1) + " " + normals.get(i + 2));
// TODO debug
	}
	
	/**
	 * Get the map latitudinal size.
	 * @return the map size across the north - south direction in meters
	 */
	public int getLatitudinalSize() {
		return latitudinalSize;
	}
	
	/**
	 * Get the map longitudinal size.
	 * @return the map size across the east - west direction in meters
	 */
	public int getLongitudinalSize() {
		return longitudinalSize;
	}
	
	/**
	 * Get the map altitudes.
	 * @return the map altitudes in meters
	 */
	public FloatBuffer getAltitudes() {
		return altitudes;
	}
	
	/**
	 * Get the map terrains.
	 * @return the map terrains
	 */
	public Terrain[] getTerrains() {
		return terrains;
	}
	
	public void dispose() {
		glDeleteBuffers(indexBuffer);
		glDeleteBuffers(vertexBuffer);
		glDeleteBuffers(normalBuffer);
		Terrain.dispose();
	}
		
	@Override
	public void render() {
//		debug();
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
		glEnableClientState(GL_VERTEX_ARRAY);
		glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
		glVertexPointer(OpenGLUtils.TRIANGLE_VERTICES, GL_FLOAT, 0, 0);
		glEnableClientState(GL_NORMAL_ARRAY);
		glBindBuffer(GL_ARRAY_BUFFER, normalBuffer);
		glNormalPointer(GL_FLOAT, 0, 0);
		
		//glClientActiveTexture(GL_TEXTURE0);
		//glEnableClientState(GL_TEXTURE_COORD_ARRAY);
		//glTexCoordPointer(2, GL_FLOAT, sizeof(MyVertex), BUFFER_OFFSET(24));   //The starting point of texcoords, 24 bytes away
		glColor3f(1.0f, 1.0f, 1.0f);		
		for (int latitude = 0; latitude < latitudinalSize; latitude++) { // TODO debug
			for (int longitude = 0; longitude < longitudinalSize; longitude++) {
				glDrawElements(GL_TRIANGLES, Direction.CENTER.ordinal() * OpenGLUtils.TRIANGLE_VERTICES, GL_UNSIGNED_INT, latitude * longitudinalSize * Direction.CENTER.ordinal() * OpenGLUtils.TRIANGLE_VERTICES * Integer.SIZE / Byte.SIZE + longitude * Direction.CENTER.ordinal() * OpenGLUtils.TRIANGLE_VERTICES * Integer.SIZE / Byte.SIZE);
			}
		}
		glColor3f(1.0f, 0.0f, 0.0f);
		glBegin(GL_LINES);
		for (int i = 0; i < vertices.capacity(); i += 3) {
			glVertex3f(vertices.get(i), vertices.get(i + 1), vertices.get(i + 2));
			glVertex3f(vertices.get(i) + normals.get(i), vertices.get(i + 1) + normals.get(i + 1), vertices.get(i + 2) + normals.get(i + 2));
		}
		glEnd();
		
		// TODO unbind normal buffers
		
		glBindBuffer(GL_ARRAY_BUFFER, 0);
		glDisableClientState(GL_NORMAL_ARRAY);
		glDisableClientState(GL_VERTEX_ARRAY);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
	}
	
	private void debug() {
		glBegin(GL_TRIANGLES);
		glColor3f(1.0f, 0.0f, 0.0f);
		glVertex3f(0.0f, 0.0f, 0.0f);
		glVertex3f(1.0f, 0.0f, 0.0f);
		glVertex3f(0.0f, 0.0f, -1.0f);
		glColor3f(0.0f, 1.0f, 0.0f);
		glVertex3f(0.0f, 0.0f, -1.0f);
		glVertex3f(1.0f, 0.0f, 0.0f);
		glVertex3f(1.0f, 0.0f, -1.0f);
		glColor3f(1.0f, 1.0f, 0.0f);
		glVertex3f(1.0f, 0.0f, 0.0f);
		glVertex3f(2.0f, 0.0f, 0.0f);
		glVertex3f(1.0f, 0.0f, -1.0f);
		glColor3f(0.0f, 0.0f, 1.0f);
		glVertex3f(1.0f, 0.0f, -1.0f);
		glVertex3f(2.0f, 0.0f, 0.0f);
		glVertex3f(2.0f, 0.0f, -1.0f);
		glEnd();
	}
}
